package com.xiyuan.smartutils.client.domain;

import com.xiyuan.smartutils.Randoms;
import com.xiyuan.smartutils.StrUtils;
import com.xiyuan.smartutils.Urls;
import com.xiyuan.smartutils.Validates;
import com.xiyuan.smartutils.constants.HttpConstants;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * form表单对象
 *
 * @author xiyuan-lgz
 */
@SuppressWarnings("ALL")
public class FormData implements HttpConstants {
    private static final byte[] BOUNDARY_SUFFIX_PREFIX = new byte[]{'-', '-'};
    private Map<String, List<Object>> paramMap = new TreeMap<>();
    private final Map<String, List<FileItem>> fileMap = new HashMap<>(2);
    
    private String boundary = "----WebMultipartFormBoundary" + Randoms.random(13);
    private Charset encoding = _UTF_8_C_;
    
    private ContentType enctype = ContentType.APPLICATION_X_WWW_FORM;
    
    private boolean autoEnctype = true;
    private boolean encoded;
    
    private JsonSerializer jsonSerializer = null;
    
    public FormData() {
    }
    
    public FormData(Charset encoding) {
        if (encoding != null) {
            this.encoding = encoding;
        }
    }
    
    public FormData(String encoding) {
        if (encoding != null) {
            this.encoding = Charset.forName(encoding);
        }
    }
    
    private Encryptor encryptor = new Encryptor() {
        @Override
        public byte[] encode(Object value) throws IOException {
            if (value == null) {return null;}
            String data = String.valueOf(value);
            return encoded ? data.getBytes(encoding) : Urls.encodeUTF8(data).getBytes(encoding);
        }
    };
    
    /**
     * 参数是否 已经 url 编码, 如果参数已经url编码则 构建 body 时则不会做url 编码。<br>
     * 只针对 application/x-www-form-urlencoded 有效
     *
     * @return true|false;
     */
    public boolean isEncoded() {
        return encoded;
    }
    
    /***
     * 参数是否 已经 url 编码, 如果参数已经url编码则 构建 body 时则不会做url 编码。<br>
     * 只针对 application/x-www-form-urlencoded 有效
     * @param encoded true|false;
     */
    public void setEncoded(boolean encoded) {
        this.encoded = encoded;
    }
    
    public String getEncoding() {
        return encoding.name().toUpperCase();
    }
    
    public void setEncoding(String encoding) {
        if (Charset.isSupported(encoding)) {
            this.encoding = Charset.forName(encoding);
        }
    }
    
    public void setEncoding(Charset encoding) {
        if (encoding != null) {
            this.encoding = encoding;
        }
    }
    
    public void setEnctype(ContentType enctype) {
        if (enctype == null) {
            return;
        }
        this.autoEnctype = false;
        this.enctype = enctype;
    }
    
    public FormData setJsonSerializer(JsonSerializer jsonSerializer) {
        this.jsonSerializer = jsonSerializer;
        return this;
    }
    
    public FormData setParamMap(Map<String, String> paramMap) {
        if (paramMap == null || paramMap.isEmpty()) {
            this.paramMap.clear();
            return this;
        }
        
        paramMap.forEach(this::append);
        return this;
    }
    
    /**
     * @param contentType enctype
     * @return this instance
     * @throws IllegalArgumentException exception when unsupport contentType
     */
    public FormData setEnctype(String contentType) throws IllegalArgumentException {
        this.enctype = ContentType.of(contentType);
        return this;
    }
    
    /**
     * 自动表单类型,当有文件参数时 自动切换 multipart/form-data
     *
     * @param autoEnctype 自动切换表单提交类型
     * @return this instance
     */
    public FormData setAutoEnctype(boolean autoEnctype) {
        this.autoEnctype = autoEnctype;
        return this;
    }
    
    public ContentType getEnctype() {
        return !autoEnctype || fileMap.isEmpty() ? enctype : ContentType.MULTIPART_FORM_DATA;
    }
    
    /**
     * 获取当前表单的ContentType <br/>
     * 返回 当有文件上传时,ContentType = "multipart/form-data"
     *
     * @return ${ContentType}; charset=${encoding}
     */
    public String getContentType() {
        ContentType enctype = getEnctype();
        return enctype.value() + "; charset=" + encoding + (ContentType.MULTIPART_FORM_DATA == getEnctype() ? "; boundary=" + boundary : "");
    }
    
    private String checkThrows(String name, String key) {
        if (StrUtils.isBlank(name)) {
            throw new IllegalArgumentException("param '" + key + "' cannot be null or empty or blank.");
        }
        
        return StrUtils.trim(name);
    }
    
    /**
     * 获取 form 表单 Boundary
     *
     * @return boundary
     */
    public String getBoundary() {
        return boundary;
    }
    
    /**
     * 设置 form 表单 Boundary
     *
     * @return this.form
     */
    public FormData setBoundary(String boundary) {
        boundary = StrUtils.trim(boundary);
        if (StrUtils.isEmpty(boundary)) {throw new IllegalArgumentException("boundary cannot be null or empty.");}
        this.boundary = boundary;
        return this;
    }
    
    /****************************************************************************************/
    /****************************
     * TODO 添加表单参数
     *****************************************/
    /****************************************************************************************/
    
    /**
     * 添加参数 添加参数
     *
     * @param condition 添加条件，如果为true 则会添加到请求中
     * @param name      参数名称 不能为空
     * @param val       参数值：注意 Object 类型会做
     * @return this instance
     * @throws IllegalArgumentException 异常信息
     */
    public FormData append(boolean condition, String name, Object val) throws IllegalArgumentException {
        if (!condition) {
            return this;
        }
        paramMap.computeIfAbsent(checkThrows(name, "name"), k -> new ArrayList<>(1)).add(val);
        return this;
    }
    
    /**
     * 添加参数
     *
     * @param name 参数名
     * @param val  参数值
     * @return this instance;
     * @throws IllegalArgumentException exception when name is empty or blank;
     */
    private FormData append(String name, Object val) throws IllegalArgumentException {
        return append(val != null, name, val);
    }
    
    /**
     * 添加参数
     *
     * @param name 参数名
     * @param val  参数值
     * @return this formData;
     * @throws IllegalArgumentException throw IllegalArgumentException when name
     *                                  is empty or blank;
     */
    public FormData append(String name, String val) throws IllegalArgumentException {
        return append(true, name, val);
    }
    
    /**
     * 添加参数
     *
     * @param name 参数名
     * @param val  参数值
     * @return this formData;
     * @throws IllegalArgumentException throw IllegalArgumentException when name
     *                                  is empty or blank;
     */
    public FormData append(String name, Boolean val) throws IllegalArgumentException {
        return this.append(name, (Object) val);
    }
    
    public FormData append(String name, Byte val) throws IllegalArgumentException {
        return this.append(name, (Object) val);
    }
    
    public FormData append(String name, Character val) throws IllegalArgumentException {
        return this.append(name, (Object) val);
    }
    
    public FormData append(String name, Short val) throws IllegalArgumentException {
        return this.append(name, (Object) val);
    }
    
    /**
     * 添加参数
     *
     * @param name 参数名
     * @param val  参数值
     * @return this formData;
     * @throws IllegalArgumentException throw IllegalArgumentException when name
     *                                  is empty or blank;
     */
    public FormData append(String name, Integer val) throws IllegalArgumentException {
        return this.append(name, (Object) val);
    }
    
    /**
     * 添加参数
     *
     * @param name 参数名
     * @param val  参数值
     * @return this formData;
     * @throws IllegalArgumentException throw IllegalArgumentException when name
     *                                  is empty or blank;
     */
    public FormData append(String name, Long val) throws IllegalArgumentException {
        return this.append(name, (Object) val);
    }
    
    /**
     * 添加参数
     *
     * @param name 参数名
     * @param val  参数值
     * @return this formData;
     * @throws IllegalArgumentException throw IllegalArgumentException when name
     *                                  is empty or blank;
     */
    public FormData append(String name, Double val) throws IllegalArgumentException {
        return this.append(name, (Object) val);
    }
    
    public FormData append(String name, Number val) throws IllegalArgumentException {
        return this.append(name, (Object) val);
    }
    
    public FormData append(String name, AtomicBoolean val) throws IllegalArgumentException {
        return this.append(name, (Object) val);
    }
    
    /****************************************************************************************/
    /************************************
     * 添加文件参数
     ******************************************/
    /****************************************************************************************/
    
    /**
     * 添加文件参数
     *
     * @param condition 条件
     * @param name      参数名
     * @param fileItem  文件参数
     * @return this instance
     */
    public FormData appendFile(boolean condition, String name, FileItem fileItem) {
        if (!condition || fileItem == null) {
            return this;
        }
        name = checkThrows(name, "name");
        fileMap.computeIfAbsent(checkThrows(name, "name"), k -> new ArrayList<>(1)).add(fileItem);
        return this;
    }
    
    /**
     * 添加文件参数
     *
     * @param name     参数名
     * @param fileItem 文件参数
     * @return this instance
     */
    public FormData appendFile(String name, FileItem fileItem) {
        return appendFile(true, name, fileItem);
    }
    
    /**
     * 添加文件参数
     *
     * @param condition 条件
     * @param name      参数名
     * @param fileName  文件名
     * @param file      文件对象
     * @return this instance
     * @throws IllegalArgumentException throw IllegalArgumentException when name
     *                                  is empty or blank;
     */
    public FormData appendFile(boolean condition, String name, String fileName, File file) {
        return appendFile(condition, name, new FileItem(fileName, file));
    }
    
    /**
     * 添加文件参数
     *
     * @param name     参数名
     * @param fileName 文件名
     * @param file     文件对象
     * @return this instance
     * @throws IllegalArgumentException throw IllegalArgumentException when name
     *                                  is empty or blank;
     */
    public FormData appendFile(String name, String fileName, File file) {
        return appendFile(true, name, fileName, file);
    }
    
    /**
     * 添加文件参数
     *
     * @param name 参数名
     * @param file 文件对象
     * @return this formData;
     * @throws IllegalArgumentException throw IllegalArgumentException when name
     *                                  is empty or blank;
     */
    public FormData appendFile(String name, File file) {
        return this.appendFile(name, file == null ? "" : file.getName(), file);
    }
    
    /**
     * 添加文件参数
     *
     * @param name        参数名
     * @param fileName    文件名
     * @param mimeType    文件mimeType
     * @param fileContent 文件类型
     * @return this formData;
     * @throws IllegalArgumentException throw IllegalArgumentException when name
     *                                  is empty or blank;
     */
    public FormData appendFile(String name, String fileName, byte[] fileContent, String mimeType) throws IllegalArgumentException {
        fileName = checkThrows(fileName, "fileName");
        return appendFile(name, new FileItem(fileName, fileContent, mimeType));
    }
    
    /**
     * 添加 文本参数
     *
     * @param paramMap 参数表
     * @return this instance
     */
    public FormData appendAll(Map<String, String> paramMap) {
        if (paramMap == null) {
            return this;
        }
        
        for (Entry<String, String> entry : paramMap.entrySet()) {
            if (Validates.isBlank(entry.getKey())) {
                continue;
            }
            this.append(entry.getKey(), entry.getValue());
        }
        
        return this;
    }
    
    public FormData appendFiles(Map<String, FileItem> fileMap) {
        if (fileMap == null) {
            return this;
        }
        
        for (Entry<String, FileItem> entry : fileMap.entrySet()) {
            if (Validates.isBlank(entry.getKey())) {
                continue;
            }
            this.appendFile(entry.getKey(), entry.getValue());
        }
        
        return this;
    }
    
    /**
     * 移除参数
     *
     * @param name 参数名
     * @return this instance
     */
    public FormData remove(String name) {
        paramMap.remove(name);
        return this;
    }
    
    /**
     * 移除文件参数
     *
     * @param name 参数名
     * @return this instance
     */
    public FormData removeFile(String name) {
        fileMap.remove(name);
        return this;
    }
    
    /**
     * 根据表单编码类型构建表单主体内容
     *
     * @return 主体内容
     */
    public String createFormBody() {
        return new String(createFormBodyToBytes(), encoding);
    }
    
    /**
     * 根据表单编码类型构建表单字节格式主体内容
     *
     * @return 主体内容字节格式
     */
    public byte[] createFormBodyToBytes() {
        ByteArrayOutputStream out = new ByteArrayOutputStream(16 * KiB);
        try {
            transfer(out);
            return out.toByteArray();
        }
        catch (Exception e) {
            throw (e instanceof RuntimeException) ? (RuntimeException) e : new RuntimeException(e);
        }
        
    }
    
    /**
     * 获取表单编码后的 内容长度
     *
     * @return 编码后的 内容长度
     */
    public long getContentLength() {
        try {
            return transfer(null);
        }
        catch (Exception e) {
            throw (e instanceof RuntimeException) ? (RuntimeException) e : new RuntimeException(e);
        }
    }
    
    /**
     * 将表单输入 到指定目标
     *
     * @param out 目标输入流
     */
    public void write(OutputStream out) throws IOException {
        this.transfer(out);
    }
    
    private long transfer(OutputStream out) throws IOException {
        ContentType enctype = getEnctype();
        if (enctype == null) {
            throw new TransferException("enctype can not be null");
        }
        switch (enctype) {
            case APPLICATION_X_WWW_FORM:
                return transferFormUrlencoded(out);
            case MULTIPART_FORM_DATA:
                return transferMultiFormData(out);
            case TEXT_PLAIN:
                return transferTextPlain(out);
            case APPLICATION_JSON:
                return transferJson(out);
            default:
                throw new TransferException("unsupported enctype: " + enctype);
        }
    }
    
    /**
     * wirte form data to output stream
     *
     * @param out   output stream
     * @param bytes bytes
     * @return if output stream is not null return byte's length; else return 0
     * @throws IOException exception
     */
    private int write(OutputStream out, byte[] bytes) throws IOException {
        if (out != null) {
            out.write(bytes);
        }
        return bytes.length;
    }
    
    /**
     * wirte form data to output stream
     *
     * @param out   output stream
     * @param bytes bytes
     * @return if output stream is not null return byte's length; else return 0
     * @throws IOException exception
     */
    private int write(OutputStream out, byte[] bytes, boolean isFirst, boolean isFilterFirst) throws IOException {
        int len = isFilterFirst && isFirst ? 0 : bytes.length;
        if (out != null && len > 0) {
            out.write(bytes);
        }
        
        return len;
    }
    
    /**
     * wirte form data to output stream
     *
     * @param out output stream
     * @param b   bytes
     * @return if output stream is not null return 1; else return 0
     * @throws IOException exception
     */
    private long write(OutputStream out, int b, boolean condition) throws IOException {
        if (out != null && condition) {
            out.write(b);
        }
        
        return condition ? 1 : 0;
    }
    
    private long transferFormUrlencoded(OutputStream out) throws IOException {
        long length = 0;
        // 添加文本参数
        for (Entry<String, List<Object>> entry : paramMap.entrySet()) {
            String key = entry.getKey();
            List<Object> values = entry.getValue();
            for (Object value : values) {
                byte[] bytes = encryptor.encode(value);
                if (bytes == null) {continue;}
                length += write(out, '&', length != 0);
                length += write(out, (Urls.encodeUTF8(key) + "=").getBytes(encoding));
                length += write(out, bytes);
            }
        }
        
        // 添加文件参数
        for (Entry<String, List<FileItem>> entry : fileMap.entrySet()) {
            List<FileItem> ls = entry.getValue();
            String key = entry.getKey();
            for (FileItem item : ls) {
                byte[] bytes = encryptor.encode(item == null ? "" : item.getFileName());
                if (bytes == null) {continue;}
                length += write(out, '&', length != 0);
                length += write(out, (Urls.encodeUTF8(key) + "=").getBytes(encoding));
                length += write(out, bytes);
            }
        }
        
        return length;
    }
    
    private long transferMultiFormData(OutputStream out) throws IOException {
        
        Encryptor encryptor = new Encryptor() {
            @Override
            public byte[] encode(Object value) throws IOException {
                if (value == null) {return null;}
                String data = String.valueOf(value);
                return data.getBytes(encoding);
            }
        };
        
        long length = 0;
        byte[] boundary_bytes = ("--" + boundary).getBytes(encoding);
        
        // 添加文本参数
        for (Entry<String, List<Object>> entry : paramMap.entrySet()) {
            String key = entry.getKey();
            List<Object> values = entry.getValue();
            
            for (Object value : values) {
                
                byte[] key_disposition_bytes = newTextDisposition(key).getBytes(encoding);
                byte[] val_disposition_bytes = encryptor.encode(value);
                if (val_disposition_bytes == null) {continue;}
                
                // write newLine when length > 0
                length += write(out, _CRLF_, length == 0, length == 0);
                length += write(out, boundary_bytes);
                
                // newLine
                length += write(out, _CRLF_);
                length += write(out, key_disposition_bytes);
                length += write(out, val_disposition_bytes);
            }
        }
        
        // 添加文件参数
        for (Entry<String, List<FileItem>> entry : fileMap.entrySet()) {
            String fieldName = entry.getKey();
            
            for (FileItem file : entry.getValue()) {
                String fileName = file.getFileName();
                String mimeType = file.getMimeType();
                
                byte[] field_disposition = newFileDisposition(fieldName, fileName, mimeType).getBytes(encoding);
                
                // write newLine when length > 0
                length += write(out, _CRLF_, length == 0, length == 0);
                length += write(out, boundary_bytes);
                
                // newLine
                length += write(out, _CRLF_);
                length += write(out, field_disposition);
                length += file.write(out);
            }
        }
        
        if (length > 0) {
            length += write(out, _CRLF_);// newLine
            length += write(out, boundary_bytes);
            length += write(out, BOUNDARY_SUFFIX_PREFIX);
            length += write(out, _CRLF_);// newLine
        }
        
        return length;
    }
    
    private long transferTextPlain(OutputStream out) throws IOException {
        
        Encryptor encryptor = new Encryptor() {
            @Override
            public byte[] encode(Object value) throws IOException {
                if (value == null) {return null;}
                String data = String.valueOf(value);
                return data.getBytes(encoding);
            }
        };
        
        long length = 0;
        
        // 添加文本参数
        for (Entry<String, List<Object>> entry : paramMap.entrySet()) {
            
            List<Object> values = entry.getValue();
            String key = StrUtils.trim(entry.getKey());
            
            for (Object value : values) {
                
                byte[] bytes = encryptor.encode(value);
                if (bytes == null) {continue;}
                // write newLine when length > 0
                length += write(out, _CRLF_, length == 0, length == 0);
                length += write(out, (key + "=").getBytes(encoding));
                length += write(out, bytes);
            }
            
        }
        
        // 添加文件参数
        for (Entry<String, List<FileItem>> entry : fileMap.entrySet()) {
            List<FileItem> values = entry.getValue();
            String key = StrUtils.trim(entry.getKey());
            for (FileItem item : values) {
                
                String value = StrUtils.trim(item == null ? null : item.getFileName());
                
                byte[] bytes = (key + "=" + value).getBytes(encoding);
                
                // write newLine when length > 0
                length += write(out, _CRLF_, length == 0, length == 0);
                
                length += write(out, bytes);
            }
            
        }
        
        return length;
    }
    
    private long transferJson(OutputStream out) throws IOException {
        if (jsonSerializer == null) {
            throw new TransferException("no jsonSerializer, you must set jsonSerializer first");
        }
        
        long length = 0;
        if (!paramMap.isEmpty()) {
            byte[] bytes = jsonSerializer.serialize(Collections.unmodifiableMap(paramMap), Collections.unmodifiableMap(fileMap), encoding, this);
            length += write(out, bytes);
        }
        
        return length;
    }
    
    /**
     * 清空表单
     *
     * @return this instance
     */
    public FormData clear() {
        paramMap.clear();
        fileMap.clear();
        
        return this;
    }
    
    /*****************************************************/
    // 提供静态调用
    /*****************************************************/
    
    /**
     * 获取上传文件时文本参数配置
     *
     * @param name 参数名
     * @return 参数配置说明
     */
    private static String newTextDisposition(String name) {
        name = StrUtils.trimToEmpty(name);
        StringBuilder strb = new StringBuilder();
        strb.append("Content-Disposition: form-data; name=\"").append(name).append("\"").append(_BR_).append(_BR_);
        return strb.toString();
    }
    
    /**
     * 获取上传文件时文件参数配置
     *
     * @param name     文件参数名
     * @param fileName 文件名
     * @param mimeType 文件类型
     * @return 参数配置说明
     */
    private static String newFileDisposition(String name, String fileName, String mimeType) {
        mimeType = StrUtils.trim(mimeType);
        name = StrUtils.trimToEmpty(name);
        fileName = StrUtils.trimToEmpty(fileName);
        StringBuilder strb = new StringBuilder();
        strb.append("Content-Disposition: form-data; name=\"").append(name).append("\"; ").append("filename=\"").append(fileName).append("\"").append(_BR_)
                .append("Content-Type:").append(Validates.isNotBlank(mimeType) ? mimeType : _APPLICATION_OCTET_STREAM_).append(_BR_).append(_BR_);
        
        return strb.toString();
    }
    
    @FunctionalInterface
    private static interface Encryptor {
        byte[] encode(Object value) throws IOException;
    }
    
    public static void main(String[] args) {
        FormData data = new FormData();
        data.append("a", "1");
        data.append("a", 1);
        data.append("a", true);
        data.append("a", "我是中国人");
        // data.appendFile("a",
        // new FileItem(new File(
        // "D:\\_gitee\\lgz\\smart-utils\\src\\main\\java\\com\\xiyuan\\smartutils\\client\\domain\\FormData.java")));
        //
        
        System.out.println(data.createFormBody());
    }
}
